#!/usr/bin/python
# -*- coding: utf-8 -*-
"""File lock for process sync.
If your OS platform is NT, you will need pywin32, please visit http://sourceforge.net/projects/pywin32/ for more detail.
"""

import os
from threading import local
try:
    from hashlib import md5
except: # for python2.4 or older version
    from md5 import md5

if os.name == 'nt':
    try:
        import win32con
        import win32file
        import pywintypes
    except ImportError:
        print "Go to http://sourceforge.net/projects/pywin32/, find pywin32.x.exe for your python version."
        raise
elif os.name == 'posix':
    import fcntl
else:
    raise RuntimeError("FileLocker only defined for nt and posix platforms.")

class HasBeenLockedError(Exception):pass

def mkmd5fromstr(s):
    if isinstance(s, unicode):
        s = s.encode('utf-8')
    return md5(s).hexdigest()

def ensure_dir_exists(dir):
    if not os.path.exists(dir):
        try:
            os.makedirs(dir)
        except OSError, e:
            if not e.args[0] in (183, 17):
                raise

                
class FileLocker(local):
    def __init__(self, locker_name, expired_seconds=0):
        if os.name == 'nt':
            locker_name = locker_name.lower()
            LOCK_FILE_PATH = os.getenv('LOCK_FILE_PATH', 'c:/tmp/filelocks')
        else:
            LOCK_FILE_PATH = os.getenv('LOCK_FILE_PATH', '/tmp/filelocks')
        path_md5 = mkmd5fromstr(locker_name)
        locker_folder = os.path.join(LOCK_FILE_PATH, path_md5[:2])
        self.__locker_filepath = os.path.join(locker_folder, path_md5)
        self.__expired_seconds = expired_seconds
        self.__file = None
        self.__is_locked = False
        ensure_dir_exists(locker_folder)

    def lock(self, break_on_failed=False):
        self.__file = file(self.__locker_filepath, "w")
        self.__lock(break_on_failed)
        self.__is_locked = True
        if self.__expired_seconds > 0:
            import threading
            t = threading.Thread(target=self.__daemon_method)
            t.setDaemon(True)
            t.start()
    acquire = lock
    
    def unlock(self):
        if not self.__is_locked: 
            return
        self.__is_locked = False
        if not self.__file: 
            return
        self.__unlock()
        self.__file.close()
        self.__file = None
    release = unlock
    
    """for with statement
    e.g.:
    with locker:
        dosomething();
    
    the same to:
    try:
        locker.lock()
    finally:
        locker.unlock()
    """
    __enter__ = acquire
    def __exit__(self, typ, val, tb):
        self.release()
    
    def __del__(self):
        self.unlock()
    
    def __daemon_method(self):
        wait_seconds = 0
        import time
        while self.__is_locked and wait_seconds < self.__expired_seconds:
            wait_seconds += 2
            time.sleep(2)
        if self.__is_locked:
            self.unlock()

    if os.name == 'nt':
        __overlapped = pywintypes.OVERLAPPED()
        def __lock(self, breakOnFailed):
            lockType = win32con.LOCKFILE_EXCLUSIVE_LOCK
            if breakOnFailed:
                lockType = lockType | win32con.LOCKFILE_FAIL_IMMEDIATELY
            hfile = win32file._get_osfhandle(self.__file.fileno())
            try:
                win32file.LockFileEx(hfile, lockType, 0, 0xf0, FileLocker.__overlapped)
            except pywintypes.error:
                self.__file.close()
                self.__file = None
                raise HasBeenLockedError("Locker has been acquired before")
    
        def __unlock(self):
            hfile = win32file._get_osfhandle(self.__file.fileno())
            win32file.UnlockFileEx(hfile, 0, 0xf0, FileLocker.__overlapped)
    
    elif os.name =='posix':
        def __lock(self, breakOnFailed):
            lockType = fcntl.LOCK_EX
            if breakOnFailed:
                lockType = lockType | fcntl.LOCK_NB
            try:
                fcntl.flock(self.__file.fileno(), lockType)
            except IOError, e:
                self.__file.close()
                self.__file = None
                import errno
                if e.errno == errno.EACCES or e.errno == errno.EAGAIN:
                    raise HasBeenLockedError("Locker has been acquired before")
                raise
    
        def __unlock(self):
            fcntl.flock(self.__file.fileno(), fcntl.LOCK_UN)

            
if __name__ == '__main__':
    i = 0
    while i < 10000:
        locker = FileLocker('testLocker._tmp_%s' % i)
        locker.lock(True)
#        import time
        print "sleep 5 ..."
#        time.sleep(1)
        locker.unlock()
        i += 1 
    print "end"
    